package com.nado.extensions.data;

import org.dom4j.Branch;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.xml.sax.XMLReader;

import com.nado.extensions.util.ResourceLoader;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;

/**
 * XML 타입의 환경 정보 데이터 모델에 대한 {@link DataHandler} 구현체.
 * <p/>
 * 하위 노드에 대한 검색은 XPath API를 이용한다.
 *
 * @see DataHandler
 */
@SuppressWarnings("unchecked")
public class Dom4jXmlDataHandler extends AbstractDataHandler {

	/**
	 * 최상위 {@link Element}
	 */
	private Node element;

	/**
	 * 설정된 위치의 파일을 이용하여 XMLDataHandler를 생성한다.
	 */
	Dom4jXmlDataHandler(String location) {
		super(location, true);
		initialize();
	}

	/**
	 * {@link Element} 파라미터를 통해 XMLDataHandler를 생성한다.
	 */
	Dom4jXmlDataHandler(Node element) {
		this.element = element;
	}

	protected void initialize() {
		ResourceLoader loader = new ResourceLoader();
		Element localElement = parseResource(location, loader);
		includeSubConfig(loader, localElement, localElement);
		this.element = localElement;
	}

	public void destroy() {
	}

	/**
	 * 파일을 읽은 후 최상위 {@link Element}를 반환한다.
	 *
	 * @param location 환경 파일의 위치
	 * @param loader   클래스 경로의 하위 자원을 검색하기 위한 {@link ClassLoader}
	 */
	private Element parseResource(String location, ResourceLoader loader) {
		InputStream inputStream;
		try {
			inputStream = getInputStream(location, loader);
		} catch (Exception e) {
			throw new DataHandlerException(e);
		}
		return parseResource(inputStream);
	}

	private Element parseResource(InputStream inputStream) {
		try {
			SAXReader saxReader = new SAXReader();
			setAllowJavaEncoding(saxReader.getXMLReader());
			return saxReader.read(inputStream).getRootElement();
		} catch (Exception e) {
			throw new DataHandlerException(e);
		}
	}

	/**
	 * xmlParser 에서 java-encodings 형식을 지원하도록 설정
	 */
	private void setAllowJavaEncoding(XMLReader xmlReader) {
		try {
			xmlReader.setFeature(
					"http://apache.org/xml/features/allow-java-encodings", true);
		} catch (Exception except) {
			log.debug("feature( allow-java-encodings) is not setted. Because Of " +
					except, except);
		} catch (AbstractMethodError error) {
			log.info("feature( allow-java-encodings) is not setted. " +
					"Because Of version of used xerces. Use xerces 2.8 or over");
		}
	}

	/**
	 * 환경 정보의 include 속성으로 포함된 환경 정보를 읽어들여 최상위 노드에 추가한다.
	 *
	 * @param loader	  클래스 경로 하위 자원을 검색하기 위한 {@link ClassLoader}
	 * @param rootElement 최상위 {@link Element}
	 * @param element	 include 요소를 포함하고 있는 {@link Element}
	 */
	private void includeSubConfig(ResourceLoader loader, Element rootElement,
								  Element element) {
		List<Element> includes = element.elements("include");

		for (Element includeInfo : includes) {
			log.debug("including file : " + includeInfo.getText());
			Element included = parseResource(includeInfo.getText(), loader);

			// inclueded config의 내용을 base config 에 붙임
			List<Element> childNodes = included.elements();
			for (Element orgChildNode : childNodes) {
				rootElement.add(orgChildNode.createCopy());
			}

			log.info("include config " + includeInfo.getText());

			// recursively include sub-config
			includeSubConfig(loader, rootElement, included);
		}
	}

	public String get(String key) {
		Node selected = element.selectSingleNode(key);
		if (selected == null) {
			throw new DataHandlerException(key + " not found in config");
		}

		String result = selected.getText();
		Matcher matcher = pattern.matcher(result);
		StringBuffer sb = new StringBuffer();
		while (matcher.find()) {
			matcher.appendReplacement(sb, get(matcher.group().substring(2,
					matcher.group().length() - 1)));
		}
		matcher.appendTail(sb);
		return sb.toString();
	}

	public boolean containsKey(String key) {
		return element.selectSingleNode(key) != null;
	}

	public List<String> getValues(String key) {
		List<Node> nodes = element.selectNodes(key);
		if (nodes == null) {
			return Collections.emptyList();
		}
		List<String> values = new ArrayList<String>(nodes.size());
		for (Node element : nodes) {
			values.add(element.getText());
		}
		return values;
	}

	public DataHandler peek(String path) {
		Node subConfigElement = element.selectSingleNode(path);
		if (subConfigElement == null) {
			return null;
		}
		return new Dom4jXmlDataHandler(subConfigElement);
	}

	public DataHandler select(String path) throws DataHandlerException {
		DataHandler handler = peek(path);
		if (handler == null) {
			throw new DataHandlerException(path + " not found in config");
		}
		return handler;
	}

	public List<DataHandler> selectAll(String path) {
		List<DataHandler> subConfigs = peekAll(path);
		if (subConfigs.isEmpty()) {
			throw new DataHandlerException(path + " not found in config");
		}
		return subConfigs;
	}

	public List<DataHandler> peekAll(String path) {
		List<Node> nodeList = element.selectNodes(path);
		if (nodeList == null) {
			return Collections.emptyList();
		}
		List<DataHandler> subConfigs = new ArrayList<DataHandler>(nodeList.size());
		for (Node element : nodeList) {
			subConfigs.add(new Dom4jXmlDataHandler(element));
		}
		return subConfigs;
	}

	public List<DataHandler> peekChildren() {
		if (!(element instanceof Branch)) {
			return Collections.emptyList();
		}
		List<DataHandler> subConfigs = new ArrayList<DataHandler>(((Branch) element).nodeCount());
		Iterator<Node> nodes = ((Branch) element).nodeIterator();
		while (nodes.hasNext()) {
			Node childNode = nodes.next();
			if (childNode.getNodeType() == Node.ELEMENT_NODE) {
				subConfigs.add(new Dom4jXmlDataHandler(childNode));
			}
		}
		return subConfigs;
	}

	public String getRootName() {
		return element.getName();
	}
}
